import type { FileStructureDiffResult } from '@/utils/fileStructure'
import { SendConnectAction } from '../action'
import type { SendConnect } from '../connect'
import path from 'path'
import type { ActionsNext } from '@/utils/action'
import config from '@/config'
import { createChunkReadIterator } from '@/utils/fs'
import { Packet } from '@/connect/packet'
import { createFailRetryMethod, sleep } from '@/utils/common'
import { FileSpinner } from '@/utils/spinner'
import { FatalError } from '@/utils/customError'

export class FileSendAction extends SendConnectAction {
  constructor(connect: SendConnect) {
    super('fileSend', connect)

    this.addAction('generateSendList', this.generateSendList_handler, true)
    this.addAction('sendFile', this.sendFile_handler)

    this.actions.addBranchChain('generateSendList', 'sendFile')
  }

  fileSendList: { path: string; hash: string }[] = []

  readonly fileSpinner = new FileSpinner(this.spinner, {
    prefix: 'Sending',
    showCount: true,
    showSpeed: true,
    averageSpeed: true
  })

  generateSendList_handler(next: ActionsNext) {
    const fileStructureDiffResult =
      this.store.get<FileStructureDiffResult>('fileStructureDiffResult')

    this.fileSendList = Object.keys(fileStructureDiffResult.sended).map((key) => {
      return {
        path: key,
        hash: fileStructureDiffResult.sended[key]
      }
    })

    this.setSpinnerText('Generating send list completed')
    next()
  }

  async sendFile_handler(next: ActionsNext) {
    this.spinner.start('Start sending files...')

    await sleep(300)

    try {
      for (let i = 0; i < this.fileSendList.length; i++) {
        const item = this.fileSendList[i]
        await this.sendFile({ path: item.path, hash: item.hash, index: i }).catch((error) => {
          if (FatalError.isFatalError(error) && error.code === 'VALIDATE_HASH_FAILED') {
            // 校验文件失败，重新发送
            this.logger.warn(`File ${item.path} hash validation failed, resend`)
            return this.sendFile({ path: item.path, hash: item.hash, index: i })
          }
          return Promise.reject(error)
        })
      }

      this.fileSpinner.end()
      this.spinner.succeed('Send files completed')
      this.logger.info('Send files completed')
      next()
    } catch (error) {
      this.logger.error(error)
      this.spinner.fail(String(error))
      this.connect.end()
    } finally {
      this.connect.stopIntervalSendPong()
    }
  }

  async sendFile(options: { path: string; hash: string; index: number }) {
    const filePath = path.join(this.connect.sendInstance.currentConfig.path, options.path)
    const chunkReadIterator = createChunkReadIterator(filePath, config.fileChunkSize)
    let count = 1
    let current = 0
    let retryCount = 0

    this.fileSpinner.update({
      relative: options.path,
      totalSize: chunkReadIterator.size,
      currentSize: current,
      currentCount: options.index + 1,
      totalCount: this.fileSendList.length
    })

    for await (const chunk of chunkReadIterator) {
      current += chunk.length
      await createFailRetryMethod(this.sendFileChunk.bind(this), config.sendFileRetryCount, () => {
        retryCount++
      })(chunk, {
        hash: options.hash,
        chunkTotal: chunkReadIterator.totalChunks,
        chunkSize: config.fileChunkSize,
        count
      })
      count++
      this.fileSpinner.update({
        relative: options.path,
        totalSize: chunkReadIterator.size,
        currentSize: current,
        currentCount: options.index + 1,
        totalCount: this.fileSendList.length,
        retryCount,
        chunkSize: chunk.length
      })
    }
  }

  async sendFileChunk(
    chunk: Buffer,
    options: { hash: string; chunkTotal: number; chunkSize: number; count: number }
  ) {
    const { hash, chunkTotal, chunkSize, count } = options

    const p = new Packet(
      {
        hash,
        chunkTotal,
        chunkSize,
        count
      },
      chunk
    )

    await this.connect.send(p)

    const rp = await this.connect.packetOnceAsync()

    if (!rp.isOk()) {
      if (rp.header.validateHashFailed) {
        throw new FatalError('', 'VALIDATE_HASH_FAILED')
      }
      const err = rp.readErrorContent() || 'sendFileChunk unknown error'
      if (rp.isFatalError()) {
        throw new FatalError(err)
      }
      throw new Error(err)
    }
  }
}
